# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import abc
import datetime
import functools
import typing
from enum import Enum

import dateutil.parser

from coin.exchange.base.kr_rest.currency import (
    NormCurrency,
    NativeCurrency,
)
from coin.exchange.base.kr_rest.from_str import FromStrProductMixin
from coin.exchange.base.kr_rest.product import NormProduct
import coin.exchange.base.kr_rest.derivative_util as derivative_util

PERPETUAL_DATE = derivative_util.PERPETUAL_DATE
IMMEDIATE_DATE = derivative_util.IMMEDIATE_DATE


class OptionsSide(Enum):
  OPTIONS_NO = 'N'
  OPTIONS_CALL = 'C'
  OPTIONS_PUT = 'P'


def _options_side_str(s):
  if s == OptionsSide.OPTIONS_NO:
    return 'N'
  elif s == OptionsSide.OPTIONS_CALL:
    return 'C'
  elif s == OptionsSide.OPTIONS_PUT:
    return 'P'
  else:
    raise ValueError(s)


def gen_short_symbol(base, quote, expiry, options_side, strike):
  short_symbol_tmpl = "{base}-{quote}.{expiry}.{options_side}.{strike}"
  assert isinstance(base, (NormCurrency, str))
  assert isinstance(quote, (NormCurrency, str))
  expiry = derivative_util.to_expiry_datetime(expiry)
  expiry_str = derivative_util.to_expiry_str(expiry)
  options_side_str = _options_side_str(options_side)

  return short_symbol_tmpl.format(
      base=base.symbol if isinstance(base, NormCurrency) else base,
      quote=quote.symbol if isinstance(quote, NormCurrency) else quote,
      expiry=expiry_str,
      options_side=options_side_str,
      strike="%d" % strike,
  )


def gen_full_symbol(base, quote, expiry, options_side, strike, exchange, native_symbol):
  full_symbol_tmpl = "{base}-{quote}.{expiry}.{options_side}.{strike}.{exchange}.{native_symbol}"
  assert isinstance(base, (NormCurrency, str))
  assert isinstance(quote, (NormCurrency, str))
  expiry = derivative_util.to_expiry_datetime(expiry)
  expiry_str = derivative_util.to_expiry_str(expiry)
  options_side_str = _options_side_str(options_side)

  return full_symbol_tmpl.format(
      base=base.symbol if isinstance(base, NormCurrency) else base,
      quote=quote.symbol if isinstance(quote, NormCurrency) else quote,
      expiry=expiry_str,
      options_side=options_side_str,
      strike="%d" % strike,
      exchange=exchange,
      native_symbol=native_symbol,
  )


def parse_options_product_symbol(symbol):
  """Parse short_symbol or full_symbol into a dict, the validity of base,
  quote and exchange are not checked.
  """
  exploded = symbol.split('.')
  assert len(exploded) in (4, 6), "Invalid symbol %s" % symbol

  base, quote = exploded[0].split('-')  # Raise if num of items does not match
  expiry = exploded[1]
  derivative_util.assert_expiry_str(expiry)
  options_side = OptionsSide(exploded[2])
  strike = float(exploded[3])

  res = {
      "base": base,
      "quote": quote,
      "expiry": expiry,
      "options_side": options_side,
      "strike": strike,
  }

  if len(exploded) == 6:
    res["exchange"] = exploded[4]
    res["native_symbol"] = exploded[5]

  return res


@functools.total_ordering
class NormOptionsProduct(object):
  def __init__(self,
               base: NormCurrency,
               quote: NormCurrency,
               expiry: typing.Union[datetime.datetime, datetime.date, str],
               options_side: OptionsSide,
               strike: float):
    assert isinstance(base, NormCurrency), base
    assert isinstance(quote, NormCurrency), quote

    self._base = base
    self._quote = quote
    self._expiry = derivative_util.to_expiry_datetime(expiry)
    self._expiry_str = derivative_util.to_expiry_str(self._expiry)
    self._options_side = options_side
    self._options_side_str = '%s' % (self._options_side)
    self._strike = strike
    self._short_symbol = gen_short_symbol(self.base,
                                          self.quote,
                                          self.expiry,
                                          self._options_side,
                                          self._strike)
    self._subscription_symbol = self._short_symbol
    self._hashed = None

  def __str__(self):
    return self.short_symbol

  def __repr__(self):
    return "NormOptionsProduct(%s, %s, %s)" % (self.base, self.quote, self.expiry_str)

  def __hash__(self):
    if self._hashed is None:
      self._hashed = hash(str(self))
    return self._hashed

  def __eq__(self, other):
    if self is other:
      return True
    elif isinstance(other, NormOptionsProduct):
      if type(self) == type(other):
        return str(self) == str(other)
      else:
        return False
    elif type(other) == str:
      return str(self) == other or self.symbol == other
    else:
      other = str(other)
      return str(self) == other or self.symbol == other

  def __lt__(self, other):
    assert isinstance(other, NormOptionsProduct)
    return str(self) < str(other)

  @property
  def base(self):
    return self._base

  @property
  def quote(self):
    return self._quote

  @property
  def expiry(self):
    return self._expiry

  @property
  def expiry_str(self):
    return self._expiry_str

  @property
  def short_symbol(self):
    return self._short_symbol

  @property
  def subscription_symbol(self):
    return self._subscription_symbol

  @property
  def symbol(self):
    return self._short_symbol

  @property
  def is_perpetual(self):
    return self.expiry == PERPETUAL_DATE

  @property
  def is_immediate(self):
    return self.expiry == IMMEDIATE_DATE


class NativeOptionsProduct(NormOptionsProduct, FromStrProductMixin):
  __metaclass__ = abc.ABCMeta

  _product_type = "Options"
  _exchange = "UNKNOWN"
  NativeCurrencyType = None

  def __init__(self,
               base,
               quote,
               expiry: typing.Union[datetime.datetime, datetime.date, str],
               options_side,
               strike,
               native_symbol: str):

    assert isinstance(base, self.NativeCurrencyType), type(base)
    assert isinstance(quote, self.NativeCurrencyType), type(quote)
    assert base != quote, (base, quote)
    assert self.NativeCurrencyType and issubclass(
        self.NativeCurrencyType, NativeCurrency), \
        'NativeCurrencyType is not set properly! %s' % self.NativeCurrencyType

    super().__init__(base=base,
                     quote=quote,
                     expiry=expiry,
                     options_side=options_side,
                     strike=strike)
    assert isinstance(native_symbol, str), native_symbol

    self._native_symbol = native_symbol
    self._native_subscription_symbol = native_symbol
    self._full_symbol = gen_full_symbol(base=self.base,
                                        quote=self.quote,
                                        exchange=self.exchange,
                                        expiry=self.expiry,
                                        options_side=options_side,
                                        strike=strike,
                                        native_symbol=self.native_symbol)

  def __str__(self):
    return self.full_symbol

  def __repr__(self, *args):
    append_args = ''
    if args:
      append_args = ', ' + ', '.join(args)
    return "%sOptionsProduct(%s, %s, %s%s)" % (
        self.exchange, self.short_symbol, self.expiry_str, self.native_symbol, append_args)

  @property
  def product_type(self):
    return type(self)._product_type

  @property
  def exchange(self):
    return type(self)._exchange

  @property
  def native_symbol(self):
    return self._native_symbol

  @property
  def native_subscription_symbol(self):
    return self._native_subscription_symbol

  @property
  def full_symbol(self):
    return self._full_symbol

  def assert_component(self, base, quote, short_symbol, full_symbol, native_symbol):
    assert self.base == base, (self.base, base)
    assert self.quote == quote, (self.quote, quote)
    assert self.short_symbol == short_symbol, (self.short_symbol, short_symbol)
    assert self.full_symbol == full_symbol, (self.full_symbol, full_symbol)
    assert self.native_symbol == native_symbol, (self.native_symbol, native_symbol)

  @classmethod
  def FromProduct(cls, product, current_datetime=None):
    current_datetime = current_datetime or datetime.datetime.now()
    return cls.FromProductImpl(product, current_datetime)

  @classmethod
  @abc.abstractmethod
  def FromProductImpl(cls, product, current_datetime):
    raise NotImplementedError()
